package com.magenta.maxoptra.yandex;

import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.commons.StringUtils;
import com.magenta.maxoptra.gis.utils.DoForwardGeocodingProfiler;
import com.magenta.maxoptra.gis.utils.GetRoutePointsByCoordsProfiler;
import com.magenta.maxoptra.gis.utils.GetRouteProfiler;
import com.magenta.maxoptra.yandex.json.parsers.ForwardGeocodingJSONParser;
import com.magenta.maxoptra.yandex.json.parsers.RoutingJSONParser;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.util.GeoInformationClient;
import org.apache.commons.lang.BooleanUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.SocketAddress;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.magenta.maxoptra.gis.GisParameter.*;
import static com.magenta.maxoptra.gis.GisParameter.locale;
import static com.magenta.maxoptra.gis.MapProviderType.yandex;
import static com.magenta.maxoptra.gis.utils.GeoProviderHelper.getGeoDouble;

/**
 * РџСЂРѕРІР°Р№РґРµСЂ РґР°РЅРЅС‹С… РѕС‚ РЇРЅРґРµРєСЃР°
 * <p/><br><br><b>
 * РџРѕР»РµР·РЅС‹Рµ СЃСЃС‹Р»РєРё:</b><br>
 * *) http://api.yandex.ru/maps/faq.xml<br>
 * *) http://clubs.ya.ru/mapsapi/
 * <p/>
 * <br><br><b>
 * Query sample:</b>
 * <p/>
 * 1) routing<br>
 * http://maps.yandex.ru/actions/get-route/?rt=50.2002685546875,53.2693268660401~50.2143310546875,53.207608024822
 * <p/>
 * 2) forward geocoding<br><br>
 * http://geocode-maps.yandex.ru/1.x/?geocode=РњРѕСЃРєРІР°,РўРІРµСЂСЃРєР°СЏ 3&results=1&key=
 * ANpUFEkBAAAAf7jmJwMAHGZHrcKNDsbEqEVjEUtCmufxQMwAAAAAAAAAAAAvVrubVT4btztbduoIgTLAeFILaQ==
 * <p/>
 * or
 * <p/>
 * http://api-maps.yandex.ru/1.1.21/xml/Geocoder/Geocoder.xml?key=
 * ANpUFEkBAAAAf7jmJwMAHGZHrcKNDsbEqEVjEUtCmufxQMwAAAAAAAAAAAAvVrubVT4btztbduoIgTLAeFILaQ==&
 * geocode=РњРѕСЃРєРІР°, СѓР».РЎР°РјРѕРєР°С‚РЅР°СЏ, Рґ.1., СЃС‚СЂ.21&ll=37.678523%2C55.75824
 * <p/>
 * 3) РЅРµРєРѕС‚РѕСЂРѕРµ РїРѕРґРѕР±РёРµ reverse РіРµРѕРєРѕРґРёРЅРіР° (С…РѕС‚СЏ РЅР° СЃР°РјРѕРј РґРµР»Рµ РїРѕР»РЅРѕС†РµРЅРЅРѕР№ РїРѕРґРґРµСЂР¶РєРё СЌС‚РѕР№ С„СѓРЅРєС†РёРё Сѓ РЇРЅРґРµРєСЃР° РЅРµС‚):<br>
 * **)http://geocode-maps.yandex.ru/1.x/?geocode=%D0%A1%D0%B0%D0%BC%D0%B0%D1%80%D0%B0&
 * ll=50.1693560283992,53.2233743800744&
 * key=ANpUFEkBAAAAf7jmJwMAHGZHrcKNDsbEqEVjEUtCmufxQMwAAAAAAAAAAAAvVrubVT4btztbduoIgTLAeFILaQ==
 * <p/>
 * **) http://maps.yandex.ru/actions/get-location-info/?lang=ru-RU&ll=30.462877%2C50.459783
 *
 * @author Zaur Muhametgaleev
 * @version 12/16/11 12:25 PM
 * @see "jira task MRS-4161: Add support of Yandex geo provider"
 * @since Maxoptra 3 (GT)
 */
public class GeoProviderYandex extends BaseGeocoding<String> implements IGeoRouting, IGeocoding, IReverseGeocoding, IGeoConfigurable {

    private Logger logger = new Logger(getClass());
    private final String INVALID_KEY = "invalid key";
    private Pattern limitPattern = Pattern.compile("results=([0-9]*)");
    private Pattern localePattern = Pattern.compile("lang=([_a-zA-Z]*)");


    protected java.net.Proxy proxy;

    @Override
    public synchronized GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to, VehicleType vehicleType) throws GeoException {
        GetRouteProfiler profiler = new GetRouteProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), from, to);
        try {
            String requestResult = getRoutingResult(from, to);
            if (StringUtils.isEmpty(requestResult)) {
                profiler.logError("null result");
                return null;
            }
            GeoRoute geoRoute = new RoutingJSONParser().extractDistanceAndDuration(requestResult, from, to);
            profiler.logDuration(geoRoute);
            return geoRoute;
        } catch (GeoException e) {
            profiler.logError(e.getMessage());
            throw e;
        }
    }

    @Override
    public synchronized List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates from,
                                                               GeographicCoordinates to) throws GeoException {
        List<GeoLocationRecord> result = new ArrayList<GeoLocationRecord>();
        GetRoutePointsByCoordsProfiler profiler = new GetRoutePointsByCoordsProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), from, to);
        try {
            String requestResult = getRoutingResult(from, to);

            if (StringUtils.isEmpty(requestResult)) {
                profiler.logError("null result");
                return result;
            }

            // add pickup point
            result.add(new GeoLocationRecord(from.getLongitude(), from.getLatitude()));
            List<GeoLocationRecord> middlePoints = new RoutingJSONParser().extractRoutePoints(requestResult);
            result.addAll(middlePoints);
            // add drop point
            result.add(new GeoLocationRecord(to.getLongitude(), to.getLatitude()));
            profiler.logDuration(result.size());
        } catch (GeoException e) {
            profiler.logError(e.getMessage());
            throw e;
        }
        return result;
    }

    private String getRoutingResult(GeographicCoordinates from, GeographicCoordinates to) {
        String url = getRoutingUrl(from, to);
        return doRequest(url);
    }

    private String doRequest(String url) {
        try {
            String encodedUrl = org.apache.commons.httpclient.util.URIUtil.encodeQuery(url);
            HttpGet get = new HttpGet(encodedUrl);
            HttpResponse response = client.execute(get);
            return EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            logger.error(e.getMessage());
        }

        return null;
    }

    private String getRoutingUrl(GeographicCoordinates from, GeographicCoordinates to) {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        String routingUrl = properties.get(routing_url.withProvider(providerName));

        if (StringUtils.isEmpty(routingUrl)) {
            throw new IllegalStateException("Routing url is not specified for provider: " + providerName);
        }

        // todo Z: add shortest route flag usage
        String fLat = getGeoDouble(from.getLatitude());
        String fLon = getGeoDouble(from.getLongitude());
        String tLat = getGeoDouble(to.getLatitude());
        String tLon = getGeoDouble(to.getLongitude());
        String url = MessageFormat.format(routingUrl, fLon, fLat, tLon, tLat);


        String enableLoggingStr = properties.get(enableLogging.withProvider(getProviderName()));
        boolean allowLogging = BooleanUtils.toBoolean(enableLoggingStr);
        if (allowLogging) {
            logger.info("Yandex routing url: " + url);
        }

        return url;
    }

    @Override
    public String getProviderName() {
        return yandex.name();
    }

    @Override
    public synchronized List<GeoAddress> findAddressByKeyword(GeoProviderSettings settings, String keyword, int i) throws GeoException {
        return getResultsList(settings, keyword, i);
    }


    @Override
    public synchronized GeoAddress findBestAddressByKeyword(GeoProviderSettings settings, String keyword) throws GeoException {
        List<GeoAddress> list = getResultsList(settings, keyword, 1);
        if (list.size() != 0) {
            return list.get(0);
        } else {
            return null;
        }
    }

    public List<GeoAddress> getResultsList(GeoProviderSettings settings, String keyword, int i) throws GeoException {

        DoForwardGeocodingProfiler profiler = new DoForwardGeocodingProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), keyword);

        String providerName = getProviderName();
        GisParameters properties = client.getProperties();
        String geocodingUrl = properties.get(geocoding_url.withProvider(providerName));
        String loc = properties.get(locale.withProvider(providerName));
        //TODO invictus: remove appending of unnecessary map_key
        String mapKey = properties.get(map_key.withProvider(providerName));
        String url = processUrl(MessageFormat.format(geocodingUrl, keyword, mapKey), i);
        if(loc != null && !loc.isEmpty()) {
            url = processLocale(url, loc);
        }

        String enableLoggingStr = properties.get(enableLogging.withProvider(getProviderName()));
        boolean allowLogging = BooleanUtils.toBoolean(enableLoggingStr);
        if (allowLogging) {
            logger.info("Yandex geocoding url: " + url);
        }

        String requestResult = doRequest(url);
        if (requestResult == null || requestResult.contains(INVALID_KEY) || requestResult.isEmpty()) {
            throw new GeoException("Invalid map key");
        }
        if (keyword == null || keyword.isEmpty()) {
            throw new GeoException("Keyword must not be empty or null");
        }
        boolean searchByResult = true;
        return new ForwardGeocodingJSONParser().modiFiedParseJson(keyword, requestResult, searchByResult, i, profiler);
    }

    private String processUrl(String url, int limit) {
        if(url == null) return null;
        Matcher m = limitPattern.matcher(url);
        if(m.find()) {
            return new StringBuilder(url).replace(m.start(1), m.end(1), String.valueOf(limit)).toString();
        } else {
            return url + "&results=" + limit;
        }
    }

    private String processLocale(String url, String locale) {
        if(url == null) return null;
        Matcher m = localePattern.matcher(url);
        if(m.find()) {
            return new StringBuilder(url).replace(m.start(1), m.end(1), locale).toString();
        } else {
            return url + "&lang=" + locale;
        }
    }

    @Override
    protected boolean accuracyIsAccurateEnough(String accuracyThreshold, String accuracy) {
        return false;
    }

    @Override
    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int i, List<String> accuracies) throws GeoException {
        throw new UnsupportedOperationException("This functionality is not supported");
    }

    @Override
    public synchronized List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address,
                                                            int resultAddressLimit) throws GeoException {
        List<String> keyList = new ArrayList<String>(4);
        List<GeoAddress> result = new ArrayList<GeoAddress>();

        if (

                address == null ||
                        (address.getCity() == null || address.getCity().isEmpty()) && (address.getCountry() == null
                                || address.getCountry().isEmpty())
                ) {
            return result;
        }
        if (address.getCountry() != null && !address.getCountry().isEmpty()) {
            keyList.add(address.getCountry());
        }
        if (address.getState() != null && !address.getState().isEmpty()) {
            keyList.add(address.getState());
        }
        if (address.getCity() != null && !address.getCity().isEmpty()) {
            keyList.add(address.getCity());
        }
        keyList.add(address.getStreet());
        keyList.add(address.getHouse());

//        String keywords = StringUtils.join(keyList,",+").replace(' ','+').replace(";","%3B");

        String keywords = StringUtils.join(keyList, ", ").replace(";", "%3B").replaceAll(",", " ");

        GeoAddress resultAddress = findBestAddressByKeyword(settings, keywords);
        if (resultAddress != null) {
            result.add(resultAddress);
        }
        return result;
    }

    @Override
    public synchronized GeoAddress doReverseGeocoding(GeographicCoordinates geoCoordinates) throws GeoException {
        return null;
    }

    @Override
    public List<GeoAddress> doReverseGeocodeFull(List<GeographicCoordinates> geoCoordinates) throws GeoException {
        throw new UnsupportedOperationException("doReverseGeocodeFull is not supported for Yandex");
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates, VehicleType vehicleType) throws GeoException {
        throw new UnsupportedOperationException("getRoute by list of coordinates is not supported for Yandex");
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoute from coordinate to list of coordinates is not supported for Yandex");
    }

    public synchronized List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, List<GeographicCoordinates> coordinates) throws GeoException {
        throw new UnsupportedOperationException("getRoutePoints by list of coordinates is not supported for Yandex");
    }

    public GeoAddress findCountry(String country, String keyword) throws GeoException {
        throw new UnsupportedOperationException("IGeocoding.findCountry is not implemented for Yandex");
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoutes from coordinate to list of coordinates is not supported for Yandex");
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates) throws ServerUnavailableException {
        throw new UnsupportedOperationException("getCoordinateToCoordinateRoutes from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException, ServerUnavailableException {
        return getRoutes(from, to, timeout);
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException {
        return getRoute(from, to, null, timeout);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates) throws GeoException {
        return getRoute(coordinates, null);
    }

    @Override
    public synchronized GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException, ServerUnavailableException {
        return getRoute(from, to, null);
    }

    @Override
    public void setClient(GeoInformationClient client) {
        this.client = client;
        HttpHost httpProxy = (HttpHost) client.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
        if (httpProxy != null) {
            String proxyHost = httpProxy.getHostName();
            int proxyPort = httpProxy.getPort();
            final Credentials credentials = ((DefaultHttpClient) client).getCredentialsProvider().getCredentials(
                    new AuthScope(proxyHost, proxyPort));

            SocketAddress socketAddress = new InetSocketAddress(proxyHost, proxyPort);
            this.proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddress);

            Authenticator.setDefault(new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    PasswordAuthentication p = new PasswordAuthentication(credentials.getUserPrincipal().getName(),
                            credentials.getPassword().toCharArray());
                    return p;
                }
            });
        }
    }
}